iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0
Modern Web

一些讓你看來很強的 ORM - prisma系列 第 21

Day21. 一些讓你看來很強的 ORM - prisma ( Seed Data)

  • 分享至 

  • xImage
  •  

今天要來介紹一個 DB 功能 seed data,有的時候當我們開發 application 時,很常會需要一些測試資料,那假設今天我們的測試資料很多,總不可能一個一個手動去新增,而且有時候也需要定期清楚過期的資料,為了簡化這些事提高開發效率,所以這時候 seed data 就派上用場了,以下我們將慢慢介紹~

介紹

seed data 大致上有以下的功能:

  • 提供 application 需要的測試資料
  • 驗證資料重複性與正確性,同時在你 migrate DB 時候 reset 你的測試資料

所以 seed data 就是讓你開發時候有資料可以測試外,同時 migrate 時候也會自動重新塞新的資料,解決 schema 不一致的問題

Demo

prisma 中如果要使用 seed data 功能,需要在 package.json 中加上 seedkeyprisma 這個欄位,然後當你要執行 seed data 的時候你只要run prisma db seed,如此就會知道你 seed data 的檔案在哪裡

// package.json
"prisma": {
  "seed": "ts-node prisma/seed.ts"
},

補充

如果使用 ts-node 的話要注意一件事情,就是 ts-node 他會幫你做 transpilingtypechecking ,那其實 typechecking 是可以被 disabled ,只需要在你的 cli 加上 --transpile-only ,這樣 ts-node 就不會執行 typechecking ,這個參數非常好用因為他可以減少 memory (RAM) 的使用,同時加快 seed data 的執行效率

"seed": "ts-node --transpile-only prisma/seed.ts"

Integrated seeding with Prisma Migrate

Datebase seedingprisma 中有兩種方式 :

  • 透過 prisma db seed 手動載入
  • prisma migrate reset 自動執行 seed data 或是 prisma migrate dev (某些情況)

使用 prisma db seed 手動方式,他的好處在於你可以測試 seed data 是否正確同時也會是你在開發前的前置作業需要確認的事情

另外當你 prisma db seed 都沒問題的時候,就可以透過 prisma migrate reset 自動執行,他會有以下的步驟:

  • 手動執行 prisma migrate reset 這個 cli
  • database 會執行 prisma migrate dev 去跟去你 migration 的記錄解決 schema 的衝突
  • 最後自動執行 prisma db seed

另外假設你不需要 seed data 在你執行,在 prisma migrate reset 或是 prisma migrate dev 只要加上 skip-seed 這個 flag 就好

Demo

以下是今天的 model

model User {
  id           String @id @default(cuid())
  name         String
  age          Int?
  profileViews Int
  country      String
  city         String
  email        String @unique
}

之後我們在 prisma 這個資料夾新增 seed.ts 的檔案,這邊簡單解釋一下 seed.ts 做了什麼事情:

  • 透過 @faker-js 產生 mock data
  • 使用 prisma.user.deleteMany 每次清空 seed data
  • 使用 prisma.user.upsertemail 有存在就跳過 craete ,確保 seed data 過程中 email 可能會有重複,原因是我們的 emailschemaunique
//prisma/seed.ts

import { faker } from "@faker-js/faker";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient()

const main = async () => {
  await prisma.user.deleteMany({})
  for (let i = 0; i < 20; i++) {
    const userEmail = faker.internet.email()
    await prisma.user.upsert({
      where: {
        email: userEmail
      },
      update: {},
      create: {
        email: userEmail,
        name: faker.person.fullName(),
        age: faker.helpers.maybe(() => faker.number.int({ min: 0, max: 100 }), { probability: 0.8 }),
        profileViews: faker.number.int({ min: 0, max: 5000 }),
        country: faker.location.country(),
        city: faker.location.city()
      }
    })
  }
}

main()
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

那因為在 age 我們的 schemaoptional 的,所以為了讓 seed 的更真實,@faker-js 有提供 maybe 的用法,有機率的 return data 用這個 utils 來模擬 user 沒有 age 的資料

所以以下的 code 會是有八成的機率 age 會有資料,否則會是 null

//..
age: faker.helpers.maybe(() => faker.number.int({ min: 0, max: 100 }), { probability: 0.8 })
//..

之後到 package.json 加上 prisma script

//package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  },
  "devDependencies": {
    "@types/node": "^14.14.21",
    "ts-node": "^9.1.1",
    "typescript": "^4.1.3"
  }
}

執行 seed script

>npx prisma db seed

接著我們到 prisma studio 檢查一下確實 age 有些資料是 null

https://ithelp.ithome.com.tw/upload/images/20241006/201456776aUSHUTosg.png

另外 ts-node 有提供不同 compiler 的選項,如果你是使用 Nextjs 的話需要根據以下的寫法去轉換一下 module

"prisma": {
  "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},

Raw SQL

其實在 prisma 中你還可以透過 Raw SQL 幫你 seed data ,使用 prisma.$executeRaw 就可以幫你完成

async function rawSql() {
  const result = await prisma.$executeRaw`INSERT INTO "User" ("id", "email", "name") VALUES (3, 'foo@example.com', 'Foo') ON CONFLICT DO NOTHING;`
  console.log({ result })
}

這邊你可以透過 Raw SQL 幫你新增額外的 seed data

main()
  .then(rawSql)
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

Seed With @snaplet/seed

另外還有一個今天的主角 @snaplet/seed 他也是另外一套 seed datatoolkit 他的好處是可以自動幫你提供 seed datatype 外,同時也幫你優化 seed data 的效率,然後 @snaplet/seed 目前支援 PostgreSQLSQLite and MySQL

使用前要先 init @snaplet/seed

>npx @snaplet/seed init prisma/seed 

他會幫你在 prisma 資料夾中產生以下的檔案:

  • seed.config.ts : seed 相關的 config
  • seed.ts : 執行 seed 的位置
    https://ithelp.ithome.com.tw/upload/images/20241006/20145677Z3eFAaw51Q.png

我們可以看到 seed.config.ts 主要就是透過 adapterconnect PrismaClient

//seed.config.ts
import { SeedPrisma } from "@snaplet/seed/adapter-prisma";
import { defineConfig } from "@snaplet/seed/config";
import { PrismaClient } from "@prisma/client";

export default defineConfig({
  adapter: () => {
    const client = new PrismaClient();
    return new SeedPrisma(client);
  },
  select: ["!*_prisma_migrations"],
});

@snaplet/seed 的寫法很單純,他會自動幫你判別你的 model 需要什麼欄位,自動幫你填上,這邊簡單解釋一下 code :

  • $resetDatabase 每次都清空 DB
  • await seed.user((createMany) => createMany(10)) 創建10筆 user 的資料
// import { createSeedClient, SeedClient } from "@snaplet/seed";
const main = async () => {

  // Truncate all tables in the database
  await seed.$resetDatabase();

  await seed.user((createMany) => createMany(10));

  // Type completion not working? You might want to reload your TypeScript Server to pick up the changes

  console.log(`Database seeded successfully!`);

  process.exit();
};

main()
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

這邊讀者打算把 ts-node 改成 tsx ,因為 tsx 他可以讓你在執行時候不用考慮 module 問題~

>npm install -D tsx

最後別忘記到 package.json 中修改一下 prismascript

//package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "prisma": {
    "seed": "tsx prisma/seed/seed.ts"
  },
  "devDependencies": {
    "@types/node": "^14.14.21",
    "tsx": "^4.7.2",
    "typescript": "^4.1.3"
  }
}

執行 seed data

>npx prisma db seed

但是筆者在查看 studio 後,發現資料內容,只是單純的塞假文字進去,這樣的 seed data 沒有還原真實資料的情況

https://ithelp.ithome.com.tw/upload/images/20241006/20145677pExzzHPZjr.png

目前筆者找到解決方式有兩種,一種是塞 env ,因為 @snaplet/seed 有支援 openai,透過 openai 優化你 response 部分

//.env
OPENAI_API_KEY="your_token"

另外一種就是透過 @faker-js ,這邊我們寫一個 seedData function ,然後你會發現我們是透過迴圈方式,去 createMany 一筆資料,那是因為假如是 createMany(100) ,對 @snaplet/seed 來說他實際並不是跑 100 次的 query 而是只有一次

const seedData = async ({
  seed,
  count
}: {
  seed: SeedClient,
  count: number
}) => {
  for (let i = 0; i < count; i++) {
    await seed.user((createMany) => createMany(1, {
      name: faker.person.fullName(),
      age: faker.number.int({ min: 0, max: 100 }),
      profileViews: faker.number.int({ min: 0, max: 5000 }),
      country: faker.location.country(),
      city: faker.location.city(),
      email: faker.internet.email()
    }));
  }
}

那這樣透過 faker 出來的資料因為執行次數的關係,資料都是一樣的,所以筆者才會改成遞迴方式,而且這樣的 email 也會有問題就不會是唯一性了

 await seed.user((createMany) => createMany(100, {
      name: faker.person.fullName(),
      age: faker.number.int({ min: 0, max: 100 }),
      profileViews: faker.number.int({ min: 0, max: 5000 }),
      country: faker.location.country(),
      city: faker.location.city(),
      email: faker.internet.email()
    }));

https://ithelp.ithome.com.tw/upload/images/20241006/20145677ehkDZzW5Bp.png

完整的 code 如下

const main = async () => {
    
  const seed = await createSeedClient();

  // Truncate all tables in the database
  await seed.$resetDatabase();
  await seedData({ seed, count: 20 })

  // Type completion not working? You might want to reload your TypeScript Server to pick up the changes

  console.log(`Database seeded successfully!`);

  process.exit();
};

另外如果你想自動執行 seed 的話,只需要在 package.jsonscript 加上 migratepostmigrate

//package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "prisma": {
    "seed": "npx tsx prisma/seed/seed.ts"
  },
  "scripts": {
    "migrate": "prisma migrate dev",
    "postmigrate": "npx @snaplet/seed sync"
  },
  "devDependencies": {
    "@types/node": "^14.14.21",
    "tsx": "^4.7.2",
    "typescript": "^4.1.3"
  }
}

這樣當你 npm run migrate 就會幫你自動 seed data

>npm run migrate  
 > prisma@1.0.0 migrate
 > prisma migrate dev

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"

Already in sync, no schema change or pending migration was found.

✔ Generated Prisma Client (v5.19.1) to ./node_modules/@prisma/client in 54ms



> prisma@1.0.0 postmigrate
> npx @snaplet/seed sync

Dynamic Seed Data

最後補充一個有趣的小 tips 我們其實可以根據下不同的 flag 在執行 prisma db seed 的時候,有不同的 seed data 的結果,只需要用 nodejs 原生的parseArgs 去讀取 cli 的參數,以下的範例就是區分 environmentdevelopmenttestdevelopment 有20筆資料 test 則是只有10筆,那讀者可以根據不同情況調整需求~

import { faker } from "@faker-js/faker";
import { PrismaClient } from "@prisma/client";

import { createSeedClient, SeedClient } from "@snaplet/seed";
import { parseArgs, ParseArgsConfig } from "util";

const main = async () => {
  const {
    values: { environment },
  } = parseArgs({ options })
  const seed = await createSeedClient();

  // Truncate all tables in the database
  await seed.$resetDatabase();

  switch (environment) {
    case 'development':
      seedData({ seed, count: 20 })
      break
    case 'test':
      seedData({ seed, count: 10 })
      break
    default:
      break
  }

  // Type completion not working? You might want to reload your TypeScript Server to pick up the changes

  console.log(`environment(${environment}): Database seeded successfully!`);

  process.exit();
};

最後只要下以下的 cli 就成功了~

>npx prisma db seed -- --environment development

大家如果有問題可以來小弟的群組討論~

✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day20. 一些讓你看來很強的 ORM - prisma (Omit Fields )
下一篇
Day22. 一些讓你看來很強的 ORM - prisma ( Logging & Debug)
系列文
一些讓你看來很強的 ORM - prisma30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言